package com.hjy.backend.dataManager.logger;

import com.google.common.primitives.Bytes;
import com.hjy.common.error.Error;
import com.hjy.backend.utils.Panic;
import com.hjy.backend.utils.Parser;

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.Arrays;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 日志的二进制文件，按照如下的格式进行排布： [XChecksum][Log1][Log2][Log3]...[LogN][BadTail]
 * 其中 XChecksum 是一个四字节的整数，是对后续所有日志计算的校验和。Log1 ~ LogN 是常规的日志数据，BadTail 是在数据库崩溃时，没有来得及写完的日志数据，这个 BadTail 不一定存在。
 * <p>
 * 每条日志的格式如下： [Size][Checksum][Data]
 * 其中，Size 是一个四字节整数，标识了 Data 段的字节数。Checksum 则是该条日志的校验和。
 * <p>
 */
public class LoggerImpl implements Logger {

    private static final int SEED = 13331; // 种子

    private static final int OF_SIZE = 0; // size 起始位置
    private static final int OF_CHECKSUM = OF_SIZE + 4; // checksum 起始位置 4
    private static final int OF_DATA = OF_CHECKSUM + 4;  // data 起始位置 8

    public static final String LOG_SUFFIX = ".log";

    private RandomAccessFile file; // 日志文件 xxx.log
    private FileChannel fc;
    private Lock lock;

    private long position;  // 当前日志指针的位置
    private long fileSize;  // 初始化时记录，log操作不更新
    private int xChecksum; // 校验和

    LoggerImpl(RandomAccessFile raf, FileChannel fc) {
        this.file = raf;
        this.fc = fc;
        lock = new ReentrantLock();
    }

    LoggerImpl(RandomAccessFile raf, FileChannel fc, int xChecksum) {
        this.file = raf;
        this.fc = fc;
        this.xChecksum = xChecksum;
        lock = new ReentrantLock();
    }

    /**
     * 日志文件初始化
     */
    public void init() {
        // 文件大小
        long size = 0;
        try {
            size = file.length();
        } catch (IOException e) {
            Panic.panic(e);
        }

        // 连 XChecksum 都没有, 肯定是坏的
        if (size < 4) {
            Panic.panic(Error.BadLogFileException);
        }

        ByteBuffer raw = ByteBuffer.allocate(4);

        try {
            fc.position(0);
            fc.read(raw); // 到达第4个字节
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        // 更新文件大小和 XChecksum
        int xChecksum = Parser.parseInt(raw.array());
        this.fileSize = size;
        this.xChecksum = xChecksum;

        // 移除 bagLog
        checkAndRemoveTail();
    }

    /**
     * 检查日志文件合法性并且移除 bagLog
     */
    private void checkAndRemoveTail() {
        rewind();

        int xCheck = 0;
        while (true) {
            byte[] log = interNext();
            if (log == null) break;
            // 依次循环, 计算出所有日志的校验和
            xCheck = calChecksum(xCheck, log);
        }
        if (xCheck != xChecksum) {
            Panic.panic(Error.BadLogFileException);
        }

        try {
            // 截断文件到正常文件的末尾
            truncate(position);
        } catch (Exception e) {
            Panic.panic(e);
        }

        try {
            file.seek(position);
        } catch (IOException e) {
            Panic.panic(e);
        }

        rewind();
    }

    /**
     * 迭代器模式, 获取下一个日志
     *
     * @return
     */
    private byte[] interNext() {
        // 到文件末尾了
        if (position + OF_DATA >= fileSize) {
            return null;
        }

        // 读取 size
        ByteBuffer buffer = ByteBuffer.allocate(4);

        try {
            fc.position(position);
            fc.read(buffer); // 读取出当前日志的大小
        } catch (IOException e) {
            Panic.panic(e);
        }

        int size = Parser.parseInt(buffer.array());

        // 当前日志超限了
        if (position + size + OF_DATA > fileSize) {
            return null;
        }

        // 读取整个日志
        ByteBuffer buffer2 = ByteBuffer.allocate(OF_DATA + size);
        try {
            fc.position(position);
            fc.read(buffer2);
        } catch (IOException e) {
            Panic.panic(e);
        }

        // 读取出的日志文件, 不过现在需要校验一下
        byte[] log = buffer2.array();
        // 获取内容部分的校验和
        int checkSum1 = calChecksum(0, Arrays.copyOfRange(log, OF_DATA, log.length));
        // 获取当前日志校验和(校验和是内容data部分的)
        int checkSum2 = Parser.parseInt(Arrays.copyOfRange(log, OF_CHECKSUM, OF_DATA));

        if (checkSum1 != checkSum2) {
            return null;
        }

        position += log.length;
        return log;
    }

    /**
     * 获取单条日志的校验和
     *
     * @param xCheck
     * @param log
     * @return
     */
    private int calChecksum(int xCheck, byte[] log) {
        for (byte b : log) {
            xCheck = xCheck * SEED + b;
        }
        return xCheck;
    }

    /**
     * 更新日志文件的校验和
     *
     * @param log
     */
    private void updateXChecksum(byte[] log) {
        this.xChecksum = calChecksum(this.xChecksum, log);
        try {
            fc.position(0);
            fc.write(ByteBuffer.wrap(Parser.int2Byte(xChecksum)));
            // 刷新缓冲区，保证内容写入磁盘
            fc.force(false);
        } catch (IOException e) {
            Panic.panic(e);
        }
    }

    /**
     * 将数据包裹成日志格式
     * <p>
     *
     * @param data
     * @return
     */
    private byte[] wrapLog(byte[] data) {
        byte[] checksum = Parser.int2Byte(calChecksum(0, data));
        byte[] size = Parser.int2Byte(data.length);
        return Bytes.concat(size, checksum, data);
    }

    @Override
    public void log(byte[] data) {
        // 包装日志数据
        byte[] log = wrapLog(data);
        // 记录到缓冲区
        ByteBuffer buffer = ByteBuffer.wrap(log);
        lock.lock();
        try {
            fc.position(fc.size());
            fc.write(buffer);
        } catch (IOException e) {
            Panic.panic(e);
        } finally {
            lock.unlock();
        }
        updateXChecksum(log);
    }

    @Override
    public void truncate(long x) throws Exception {
        lock.lock();
        try {
            fc.truncate(x);
        } finally {
            lock.unlock();
        }
    }

    @Override
    public byte[] next() {
        lock.lock();
        try {
            byte[] log = interNext();
            if(log == null) return null;
            return Arrays.copyOfRange(log, OF_DATA, log.length);
        } finally {
            lock.unlock();
        }
    }

    @Override
    public void rewind() {
        position = 4;
    }

    @Override
    public void close() {
        try {
            fc.close();
            file.close();
        } catch (IOException e) {
            Panic.panic(e);
        }
    }
}
